/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.swan.plugin;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.type.MapType;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.swan.plugin.bean.Pojo;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import static org.swan.plugin.ConfigFileMojo.DEFAULT_CONFIG_JSON;

/**
 * Goal which generate pojo file from db
 */
@Mojo(name = "db", defaultPhase = LifecyclePhase.PROCESS_SOURCES)
public class DbMojo extends AbstractMojo {
    public static final String TABLE_NAME = "TABLE_NAME";
    @Parameter(defaultValue = "${project.basedir}/template", property = "db.dataPath", required = true)
    private File basedir;
    @Parameter(defaultValue = "", property = "db.driver", required = true)
    private String driver;
    @Parameter(defaultValue = "", property = "db.url", required = true)
    private String url;
    @Parameter(defaultValue = "", property = "db.user", required = true)
    private String user;
    @Parameter(defaultValue = "", property = "db.password", required = true)
    private String password;
    @Parameter(defaultValue = "", property = "db.database", required = true)
    private String database;
    @Parameter(defaultValue = "", property = "db.packageName", required = true)
    private String packageName;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        Map<String, Pojo> defaultData = new TreeMap<>(Comparator.comparingInt(String::length));
        defaultData.putAll(loadDefaultData());
        try {
            Class.forName(driver);
            try (Connection connection = DriverManager.getConnection(url, user, password)) {
                DatabaseMetaData metaData = connection.getMetaData();
                ResultSet resultSet = metaData.getColumns(database, null, null, null);
                String tableName = null;
                Pojo pojo = null;
                while (resultSet.next()) {
                    if (!resultSet.getString(TABLE_NAME).equals(tableName)) {
                        pojo = processPojo(defaultData, resultSet, tableName, pojo);
                    }
                    pojo.addField(resultSet.getString("COLUMN_NAME"), resultSet.getString("REMARKS"), typeMapper(resultSet.getInt("DATA_TYPE")));
                    tableName = resultSet.getString(TABLE_NAME);
                }
            }
        } catch (ClassNotFoundException | SQLException | IOException e) {
            getLog().error(e);
        }
    }

    /**
     * Save last Pojo and create new Pojo
     * @param defaultData default data in config file
     * @param resultSet table information
     * @param tableName table name
     * @param pojo last pojo
     * @return new Pojo instance
     * @throws IOException
     * @throws SQLException
     */
    private Pojo processPojo(Map<String, Pojo> defaultData, ResultSet resultSet, String tableName, Pojo pojo) throws IOException, SQLException {
        exportPojo(defaultData, tableName, pojo);
        return getNewPojo(resultSet);
    }

    /**
     * export pojo to file/files
     * @param defaultData default data in config file
     * @param tableName table name
     * @param pojo pojo instance
     * @throws IOException
     */
    private void exportPojo(Map<String, Pojo> defaultData, String tableName, Pojo pojo) throws IOException {
        if (pojo != null) {
            //Save to file
            File f = basedir;
            if (!f.exists())
                f.mkdirs();
            f = new File(f, tableName + ".pojo.json");
            if (!f.exists()) {
                ObjectMapper om = new ObjectMapper();
                AtomicReference<Pojo> tmp = new AtomicReference<>();
                final Pojo finalPojo = pojo;
                defaultData.entrySet().stream().filter(entry -> Pattern.compile(entry.getKey()).matcher(finalPojo.getId()).matches()).forEach(entry -> tmp.set(mergePojo(tmp.get(), entry.getValue(), om)));
                tmp.set(mergePojo(tmp.get(), pojo, om));
                new ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(new FileWriter(f), new Pojo[]{tmp.get()});
                getLog().info(f.getName() + " created successfully.");
            }
        }
    }

    /**
     * Create Pojo instance from table information
     * @param resultSet table information
     *
     * @return
     * @throws SQLException
     */
    private Pojo getNewPojo(ResultSet resultSet) throws SQLException {
        String className = resultSet.getString(TABLE_NAME);
        className = Character.toUpperCase(className.charAt(0)) + className.substring(1);
        Pojo.TemplateBean templateBean = new Pojo.TemplateBean().setClassName(className);
        Pojo newPojo = new Pojo().setId(className).addTemplate(templateBean);
        if (packageName != null && !packageName.trim().isEmpty()) {
            templateBean.setPackageName(packageName);
        }
        return newPojo;
    }

    private Pojo mergePojo(Pojo source, Pojo override, ObjectMapper objectMapper) {
        if (source == null) {
            return override;
        }
        if (override == null)
            return source;
        ObjectReader updater = objectMapper.readerForUpdating(source);
        try {
            return updater.readValue(objectMapper.writeValueAsString(override));
        } catch (JsonProcessingException e) {
            return source;
        }
    }

    private Map<String, ? extends Pojo> loadDefaultData() {
        ObjectMapper om = new ObjectMapper();
        MapType mapType = om.getTypeFactory().constructMapType(Map.class, String.class, Pojo.class);
        File f = new File(basedir, DEFAULT_CONFIG_JSON);
        if (f.exists()) {
            try {
                return om.readValue(f, mapType);
            } catch (IOException e) {
                return new HashMap<>();
            }
        }
        return new HashMap<>();
    }

    private String typeMapper(int type) {
        switch (type) {
            case Types.BIT:
            case Types.BOOLEAN:
                return "boolean";
            case Types.INTEGER:
            case Types.TINYINT:
                return "int";
            case Types.BIGINT:
                return "long";
            case Types.DECIMAL:
            case Types.DOUBLE:
            case Types.FLOAT:
                return "double";
            case Types.BLOB:
            case Types.CLOB:
            case Types.VARBINARY:
            case Types.BINARY:
            case Types.LONGVARBINARY:
                return "byte[]";
            case Types.VARCHAR:
            case Types.NVARCHAR:
            case Types.LONGVARCHAR:
            case Types.LONGNVARCHAR:
                return "String";
            default:
                return "String";
        }
    }
}
